﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Web;
using System.Windows.Forms;
using Selenium;

namespace QuailSeleniumManager
{
    /// <summary>
    /// The main form turn on
    /// </summary>
    public partial class FormMain : Form
    {
        private readonly object _syncLock = new object();
        private readonly List<string> _tempFiles = new List<string>();
        private readonly List<FireFoxDialog> _firefoxDialogsToClose;
        private readonly HttpServer _server = new HttpServer();
        private readonly QuailServiceReference.QuailDataServiceClient _serviceClient = new QuailServiceReference.QuailDataServiceClient();
        private bool _injoinCluster;
        private Process _seleniumProcess;
        private bool _autoRestart = true, _wasSeleniumRunning, _loaded;
        private int _autorestartCounter;

        /// <summary>
        /// Initializes a new instance of the <see cref="FormMain"/> class.
        /// </summary>
        public FormMain()
        {
            InitializeComponent();
            _firefoxDialogsToClose = new List<FireFoxDialog>
                                         {
                                             new FireFoxDialog("Close Firefox", "Stuck Firefox"),
                                             new FireFoxDialog("Confirm", "Reload page confirmation"),
                                             new FireFoxDialog("Mozilla Crash Reporter", "Mozilla Crash page"),
                                         };
        }

        private delegate void OnSeleniumDidSomething();

        private bool IsSeleniumRunning
        {
            get
            {
                lock (_syncLock)
                {
                    if (_seleniumProcess == null)
                        AttachToProcess();

                    return _seleniumProcess != null;
                }
            }
        }

        private void ButtonStopStart_Click(object sender, EventArgs e)
        {
            if (IsSeleniumRunning)
            {
                CheckBoxAutoRestart.Checked = false;
                SeleniumStop(false);
            }
            else
            {
                SeleniumStart();
                CheckBoxAutoRestart.Checked = true;
            }
        }

        private void SeleniumStart()
        {
            try
            {
                var startcmd = GetSeleniumBinaries();
                _autoRestart = false;
                var psi = new ProcessStartInfo(startcmd)
                              {
                                  WorkingDirectory = Path.GetDirectoryName(startcmd),
                                  UseShellExecute = false
                              };
                Process.Start(psi);
                AddLog("Started Selenium");
                Thread.Sleep(500);
                SeleniumUpdateStatus();
            }
            catch (Exception e)
            {
                AddLog(e);
            }
        }

        private void AddLog(Exception e)
        {
            AddLog("Exception: " + e.Message);
        }

        private void AddLog(string s)
        {
            while (TextBoxLog.Lines.Length > 100)
            {
                TextBoxLog.Select(0, TextBoxLog.GetFirstCharIndexFromLine(1));
                TextBoxLog.SelectedText = string.Empty;
            }

            var msg = DateTime.Now + ": " + s + "\r\n";
            TextBoxLog.Select(TextBoxLog.TextLength, 0);
            TextBoxLog.SelectedText = msg;
        }

        private void SeleniumRecycle()
        {
            if (IsSeleniumRunning)
                SeleniumStop(true);
            else
                SeleniumStart();
        }

        private void SeleniumStop()
        {
            SeleniumStop(false);
        }

        private void SeleniumStop(bool autorestart)
        {
            try
            {
                _autoRestart = autorestart;
                var sel = new DefaultSelenium("localhost", 4444, "*chrome", "http://www.google.com");
                sel.ShutDownSeleniumServer();
                AddLog("Stopped Selenium");
            }
            catch (Exception e)
            {
                AddLog(e);
            }
        }

        private static IntPtr FindWindow(string windowtitle)
        {
            IntPtr foundHwnd = IntPtr.Zero;
            var sb = new StringBuilder(1024);

            EnumWindows.EnumChildWindowsProc cb = hwnd =>
            {
                if (IsWindowVisible(hwnd))
                {
                    GetWindowText(hwnd, sb, sb.Capacity);

                    if (sb.Length > 0)
                    {
                        var sbtitle = sb.ToString();
                        if (sbtitle == windowtitle)
                        {
                            foundHwnd = hwnd;
                            return false;
                        }
                    }
                }

                return true;
            };
            EnumWindows.EnumChildWindows(cb, IntPtr.Zero);

            return foundHwnd;
        }

        private void AttachToProcess()
        {
            lock (_syncLock)
            {
                var foundHwnd = FindWindow("Selenium Server");

                if (foundHwnd == IntPtr.Zero)
                    foundHwnd = FindWindow("Administrator:  Selenium Server");

                if (foundHwnd != IntPtr.Zero)
                {
                    int processid;
                    if (GetWindowThreadProcessId(foundHwnd, out processid) != 0)
                    {
                        _seleniumProcess = Process.GetProcessById(processid);
                        _seleniumProcess.EnableRaisingEvents = true;
                        _seleniumProcess.Exited += SeleniumProcess_Exited;

                        AddLog("Selenium is started.");
                        JoinClusterIfConfigured();
                    }
                }
            }
        }

        private void SeleniumProcessExitCompleted()
        {
            lock (_syncLock)
            {
                _seleniumProcess = null;
                SeleniumUpdateStatus();
            }
        }

        private void SeleniumProcess_Exited(object sender, EventArgs e)
        {
            Invoke(new OnSeleniumDidSomething(SeleniumProcessExitCompleted));
        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            CheckBoxAutoRestart.Checked = Properties.Settings.Default.AutoRestart;
            CheckBoxJoin.Checked = Properties.Settings.Default.JoinQuailCluster;

            _autoRestart = CheckBoxAutoRestart.Checked;
            SeleniumUpdateStatus();
            CheckTimerStatus();

            _server.Start("http://*:4443/");
            _server.ReceiveWebRequest += ReceiveWebRequest;
            AddLog("WebServer initiated on http://" + Environment.MachineName + ":4443/");
            _loaded = true;
            JoinClusterIfConfigured();
        }

        private void ReceiveWebRequest(System.Net.HttpListenerContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var reply = "<html><title>Quail Selenium Manager (" + Environment.MachineName + ")</title><body>";
            if (!string.IsNullOrEmpty(request.QueryString["recycle"]))
            {
                Invoke(new OnSeleniumDidSomething(SeleniumRecycle));
            }
            else if (!string.IsNullOrEmpty(request.QueryString["start"]))
            {
                Invoke(new OnSeleniumDidSomething(SeleniumStart));
            }
            else if (!string.IsNullOrEmpty(request.QueryString["stop"]))
            {
                Invoke(new OnSeleniumDidSomething(SeleniumStop));
            }
            else if (!string.IsNullOrEmpty(request.QueryString["join"]))
            {
                Invoke(new OnSeleniumDidSomething(JoinClusterNow));
            }
            else if (!string.IsNullOrEmpty(request.QueryString["leave"]))
            {
                Invoke(new OnSeleniumDidSomething(LeaveCluster));
            }
            else if (!string.IsNullOrEmpty(request.QueryString["clearlog"]))
            {
                Invoke(new OnSeleniumDidSomething(ClearLog));
            }

            reply += "<p><h1>" + Environment.MachineName + " : " + (IsSeleniumRunning ? "Online" : "Offline") + "</h1></p>";
            reply += "<p>Current time: " + DateTime.Now + ".<br/>What would you like to do?<p>";
            reply += "<table border=0 cellpadding=3 cellspacing=2><tr><td><ul>";
            reply += "<li> <a href=\"?\">Refresh</a>";
            reply += "<li> <a href=\"?recycle=1\">Recycle Selenium</a>";
            reply += "<li> <a href=\"?stop=1\">Stop Selenium</a>";
            reply += "<li> <a href=\"?start=1\">Start Selenium</a>";
            reply += "<li> <a href=\"?join=1\">Join Cluster</a>";
            reply += "<li> <a href=\"?leave=1\">Leave Cluster</a>";
            reply += "<li> <a href=\"?clearlog=1\">Clear Log</a>";
            reply += "</ul></td><td><ul>";

            var hosts = GetRemoteControlHosts();
            foreach (var host in hosts)
            {
                reply += "<li> <a href=\"http://" + host.Host + ":4443/\">" + host.Host + "</a> ";
                if (!host.IsAvailable)
                    reply += " <i>not clustered</i>";

                if (host.Usage > 0)
                    reply += " (" + host.Usage + ")";
            }

            reply += "</ul></td></tr></table>";
            
            reply += "<textarea rows=20 style=\"font-family: Arial;width:100%\">";
            reply += HttpUtility.HtmlEncode(TextBoxLog.Text);
            reply += "</textarea>";
            reply += "</body>";

            var output = Encoding.UTF8.GetBytes(reply);

            response.ContentType = "text/html";
            response.ContentLength64 = output.Length;

            var stream = response.OutputStream;
            stream.Write(output, 0, output.Length);
            stream.Close();
        }

        private void ClearLog()
        {
            TextBoxLog.Clear();
        }

        private void FormMain_Resize(object sender, EventArgs e)
        {
            CheckTimerStatus();
        }

        private void CheckTimerStatus()
        {
            TimerStatus.Enabled = CheckBoxAutoRestart.Checked || WindowState != FormWindowState.Minimized;
        }

        private void TimerStatus_Tick(object sender, EventArgs e)
        {
            SeleniumUpdateStatus();
        }

        private string GetSeleniumBinaries()
        {
            var path = Path.GetTempPath();

            try
            {
                GetAndWriteFile(path, "selenium-server.jar");
                GetAndWriteFile(path, "user-extensions.js");

                const string bat = "@echo off\ntitle Selenium Server\njava -jar selenium-server.jar -interactive -userExtensions user-extensions.js\n";

                var batname = Path.GetTempFileName();
                File.Delete(batname);
                batname += ".bat";
                var stream = new StreamWriter(batname);
                stream.Write(bat);
                stream.Close();

                AddLog("Wrote " + batname);
                _tempFiles.Add(batname);

                return batname;
            }
            catch (Exception e)
            {
                AddLog("Exception during GetSeleniumBinaries() " + e.Message);
                throw;
            }
        }

        private void GetAndWriteFile(string path, string filename)
        {
            var file = _serviceClient.GetBinaryFile(filename);

            var stream = new FileStream(path + filename, FileMode.Create);
            _tempFiles.Add(path + filename);
            try
            {
                stream.Write(file._data, 0, file._data.Length);
                AddLog("Wrote " + file._data.Length + " bytes to " + stream.Name);
            }
            finally
            {
                stream.Close();
            }
        }

        private void SeleniumUpdateStatus()
        {
            var running = IsSeleniumRunning;
            LabelStatus.Text = running ? "Online" : "Offline";
            LabelStatus.ForeColor = Color.FromKnownColor(running ? KnownColor.ControlText : KnownColor.Red);

            if (_wasSeleniumRunning && !running)
            {
                AddLog("Selenium stopped.");
                LeaveCluster();
            }

            ButtonStopStart.Text = running ? "Stop" : "Start";

            _wasSeleniumRunning = running;

            if (!running && (_autoRestart || CheckBoxAutoRestart.Checked))
            {
                _autorestartCounter++;

                if (_autorestartCounter < 5)
                    SeleniumStart();
                else if (_autorestartCounter == 60)
                    _autorestartCounter = 0;
            }
            else
                _autorestartCounter = 0;
        }

        private void ButtonRecycle_Click(object sender, EventArgs e)
        {
            SeleniumRecycle();
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool IsWindowVisible(IntPtr hwnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int GetWindowText(IntPtr hwnd, StringBuilder lpstring, int nmaxCount);

        [DllImport("user32.dll")]
        private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out int lpdwProcessId);

        private void FormMain_FormClosed(object sender, FormClosedEventArgs e)
        {
            JoinCluster(false);

            if (_seleniumProcess != null)
                _seleniumProcess.Exited -= SeleniumProcess_Exited;

            SeleniumStop(false);

            foreach (var filename in _tempFiles)
            {
                for (int trycounter = 0; trycounter < 10; trycounter++)
                {
                    try
                    {
                        File.Delete(filename);
                        break;
                    }
                    catch (Exception)
                    {
                        Thread.Sleep(100);
                    }
                }
            }

            Properties.Settings.Default.AutoRestart = CheckBoxAutoRestart.Checked;
            Properties.Settings.Default.Save();
        }

        private void CheckBoxJoin_CheckedChanged(object sender, EventArgs e)
        {
            if (IsSeleniumRunning && CheckBoxJoin.Checked)
                JoinCluster(true);
            else if (!CheckBoxJoin.Checked)
                JoinCluster(false);
        }

        private void JoinClusterNow()
        {
            CheckBoxJoin.Checked = true;
        }

        private void LeaveCluster()
        {
            CheckBoxJoin.Checked = false;
        }

        private void JoinClusterIfConfigured()
        {
            JoinCluster(Properties.Settings.Default.JoinQuailCluster || CheckBoxJoin.Checked);
        }

        private List<RcHostInfo> GetRemoteControlHosts()
        {
            var result = new List<RcHostInfo>();
            
            try
            {
                var status = _serviceClient.GetCurrentRemoteControlStatus();

                foreach (var item in status)
                {
                    var rc = new RcHostInfo
                                 {
                                     Host = item.HostName,
                                     IsAvailable = item.IsAvailable,
                                     Usage = item.Load.GetValueOrDefault(0)
                                 };
                    result.Add(rc);
                }
            }
            catch (Exception)
            {
            }

            return result;
        }

        private void JoinCluster(bool join)
        {
            if (_injoinCluster || !_loaded)
                return;

            try
            {
                _injoinCluster = true;

                var success = _serviceClient.EnableRemoteControl(Environment.MachineName, join);

                if (success)
                    AddLog((join ? "Joined" : "Left") + " RC cluster");
                else if (join)
                {
                    _serviceClient.AddRemoteControl(Environment.MachineName, 4444, null);
                    AddLog("Joined RC cluster");
                }

                CheckBoxJoin.Checked = join;
            }
            catch (Exception ex)
            {
                AddLog("Exception attemping to " + (join ? "join" : "leave") + " cluster: " + ex.Message);
                CheckBoxJoin.Checked = !join;
            }
            finally
            {
                _injoinCluster = false;

            }
        }

        private void TimerFirefoxBug_Tick(object sender, EventArgs e)
        {
            TimerFirefoxBug.Enabled = false;
            try
            {
                lock (_syncLock)
                {
                    foreach (var dlg in _firefoxDialogsToClose)
                    {
                        var foundHwnd = FindWindow(dlg.Caption);
                        if (foundHwnd != IntPtr.Zero)
                        {
                            if (PostMessage(foundHwnd, 16, 0, 0))
                                AddLog("Closed FireFox window: " + dlg.DialogName);
                            else
                            {
                                AddLog("Failed to close FireFox window: " + dlg.DialogName + ": " + Marshal.GetLastWin32Error());
                            }
                        }                        
                    }
                }
            }
            catch (Exception ex)
            {
                AddLog("Exception during timer firefox bug:" + ex.Message);
            }
            finally
            {
                TimerFirefoxBug.Enabled = true;
            }
        }

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool PostMessage(IntPtr hwnd, uint msg, int wparam, int lparam);

        /// <summary>
        /// Remote control host information
        /// </summary>
        private class RcHostInfo
        {
            /// <summary>
            /// Gets or sets Host.
            /// </summary>
            public string Host
            {
                get; set;
            }

            /// <summary>
            /// Gets or sets a value indicating whether IsAvailable.
            /// </summary>
            public bool IsAvailable
            {
                get; set;
            }

            /// <summary>
            /// Gets or sets Usage.
            /// </summary>
            public int Usage
            {
                get; set;
            }
        }

        /// <summary>
        /// Firefox dialog to close
        /// </summary>
        private class FireFoxDialog
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="FireFoxDialog"/> class.
            /// </summary>
            /// <param name="caption">The caption.</param>
            /// <param name="dialogname">The dialogname.</param>
            public FireFoxDialog(string caption, string dialogname)
            {
                Caption = caption;
                DialogName = dialogname;
            }

            /// <summary>
            /// Gets the Caption for the dialog
            /// </summary>
            public string Caption
            {
                get;
                private set;
            }

            /// <summary>
            /// Gets the name of the dialog
            /// </summary>
            public string DialogName
            {
                get;
                private set;
            }
        }
    }
}
